{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a6905641",
   "metadata": {},
   "source": [
    "# DAY 45 Tensorborad\n",
    "\n",
    "之前的内容中，我们在神经网络训练中，为了帮助自己理解，借用了很多的组件，比如训练进度条、可视化的loss下降曲线、权重分布图，运行结束后还可以查看单张图的推理效果。\n",
    "\n",
    "如果现在有一个交互工具可以很简单的通过按钮完成这些辅助功能那就好了。所以我们现在介绍下tensorboard这个可视化工具，他可以很方便的很多可视化的功能，尤其是他可以在运行过程中实时渲染，方便我们根据图来动态调整训练策略，而不是训练完了才知道好不好。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5367c4ed",
   "metadata": {},
   "source": [
    "## 一、tensorboard的基本操作"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72804546",
   "metadata": {},
   "source": [
    "### 1.1 发展历史"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06259c2e",
   "metadata": {},
   "source": [
    "TensorBoard 是 TensorFlow 生态中的官方可视化工具（也可无缝集成 PyTorch），用于实时监控训练过程、可视化模型结构、分析数据分布、对比实验结果等。它通过网页端交互界面，将枯燥的训练日志转化为直观的图表和图像，帮助开发者快速定位问题、优化模型。\n",
    "\n",
    "简单来说，TensorBoard 是 TensorFlow 自带的一个「可视化工具」，就像给机器学习模型训练过程装了一个「监控屏幕」。你可以用它直观看到训练过程中的数据变化（比如损失值、准确率）、模型结构、数据分布等，不用盯着一堆枯燥的数字看，对新手非常友好。\n",
    "\n",
    "TensorBoard 的发展历程如下：\n",
    "- 2015 年随着 TensorFlow 框架一起发布，最初是为了满足深度学习研究者可视化复杂模型训练过程的需求。2016-2018 年新增了更多可视化功能，图像 / 音频可视化：可以直接看训练数据里的图片、听音频（比如在图像分类任务中，查看输入的图片是否正确）。\n",
    "直方图：展示数据分布（比如权重参数的分布是否合理）。\n",
    "多运行对比：同时对比多个训练任务的结果（比如不同学习率的效果对比）。\n",
    "\n",
    "- 2019 年后与 PyTorch 兼容，变得更通用了。功能进一步丰富，比如支持3D 可视化、模型参数调试等。\n",
    "\n",
    "目前这个工具还在不断发展，比如一些额外功能在tensorboardX上存在，但是我们目前只需要要用到最经典的几个功能即可\n",
    "\n",
    "1. 保存模型结构图\n",
    "2. 保存训练集和验证集的loss变化曲线，不需要手动打印了\n",
    "3. 保存每一个层结构权重分布\n",
    "4. 保存预测图片的预测信息\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01789583",
   "metadata": {},
   "source": [
    "### 1.2 tensorboard的原理"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b97c977",
   "metadata": {},
   "source": [
    "TensorBoard 的核心原理就是在训练过程中，把训练过程中的数据（比如损失、准确率、图片等）先记录到日志文件里，再通过工具把这些日志文件可视化成图表，这样就不用自己手动打印数据或者用其他工具画图。\n",
    "\n",
    "所以核心就是2个步骤：\n",
    "- 数据怎么存？—— 先写日志文件\n",
    "\n",
    "训练模型时，TensorBoard 会让程序把训练数据（比如损失值、准确率）和模型结构等信息，写入一个特殊的日志文件（.tfevents 文件）\n",
    "\n",
    "- 数据怎么看？—— 用网页展示日志\n",
    "\n",
    "写完日志后，TensorBoard 会启动一个本地网页服务，自动读取日志文件里的数据，用图表、图像、文本等形式展示出来。如果只用 print(损失值) 或者自己用 matplotlib 画图，不仅麻烦，还得手动保存数据、写代码，尤其训练几天几夜时，根本没法实时盯着看。而 TensorBoard 能自动把这些数据 “存下来 + 画出来”，还能生成网页版的可视化界面，随时刷新查看！\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bde86dbf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# pip install tensorboard -i https://pypi.tuna.tsinghua.edu.cn/simple"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b30b4e4",
   "metadata": {},
   "source": [
    "下面是tensorboard的核心代码解析，无需运行 看懂大概在做什么即可"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b742cba0",
   "metadata": {},
   "source": [
    "### 1.3  日志目录自动管理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "090788ac",
   "metadata": {},
   "outputs": [],
   "source": [
    "log_dir = 'runs/cifar10_mlp_experiment'\n",
    "if os.path.exists(log_dir):\n",
    "    i = 1\n",
    "    while os.path.exists(f\"{log_dir}_{i}\"):\n",
    "        i += 1\n",
    "    log_dir = f\"{log_dir}_{i}\"\n",
    "writer = SummaryWriter(log_dir) #关键入口，用于写入数据到日志目录"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d183148e",
   "metadata": {},
   "source": [
    "自动避免日志目录重复。若 runs/cifar10_mlp_experiment 已存在，会生成 runs/cifar10_mlp_experiment_1、_2 等新目录，确保每次训练的日志独立存储。\n",
    "\n",
    "方便对比不同训练任务的结果（如不同超参数实验）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80366ae4",
   "metadata": {},
   "source": [
    "### 1.4 记录标量数据（Scalar）\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f8cba65f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 记录每个 Batch 的损失和准确率\n",
    "writer.add_scalar('Train/Batch_Loss', batch_loss, global_step)\n",
    "writer.add_scalar('Train/Batch_Accuracy', batch_acc, global_step)\n",
    "\n",
    "# 记录每个 Epoch 的训练指标\n",
    "writer.add_scalar('Train/Epoch_Loss', epoch_train_loss, epoch)\n",
    "writer.add_scalar('Train/Epoch_Accuracy', epoch_train_acc, epoch)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "068ceb49",
   "metadata": {},
   "source": [
    "在 tensorboard的SCALARS 选项卡中查看曲线，支持多 run 对比。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ba8b4c1",
   "metadata": {},
   "source": [
    "### 1.5 可视化模型结构（Graph）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "794574d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "dataiter = iter(train_loader)\n",
    "images, labels = next(dataiter)\n",
    "images = images.to(device)\n",
    "writer.add_graph(model, images)  # 通过真实输入样本生成模型计算图"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "470b86d0",
   "metadata": {},
   "source": [
    "TensorBoard 界面：在 GRAPHS 选项卡中查看模型层次结构（卷积层、全连接层等）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "305259b1",
   "metadata": {},
   "source": [
    "### 1.6 可视化图像（Image）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3a1e00a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 可视化原始训练图像\n",
    "img_grid = torchvision.utils.make_grid(images[:8].cpu()) # 将多张图像拼接成网格状（方便可视化），将前8张图像拼接成一个网格\n",
    "writer.add_image('原始训练图像', img_grid)\n",
    "\n",
    "# 可视化错误预测样本（训练结束后）\n",
    "wrong_img_grid = torchvision.utils.make_grid(wrong_images[:display_count])\n",
    "writer.add_image('错误预测样本', wrong_img_grid)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a06f4ca9",
   "metadata": {},
   "source": [
    "展示原始图像、数据增强效果、错误预测样本等。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73178526",
   "metadata": {},
   "source": [
    "### 1.7 记录权重和梯度直方图（Histogram）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6ce9ae6",
   "metadata": {},
   "outputs": [],
   "source": [
    "if (batch_idx + 1) % 500 == 0:\n",
    "    for name, param in model.named_parameters():\n",
    "        writer.add_histogram(f'weights/{name}', param, global_step)  # 权重分布\n",
    "        if param.grad is not None:\n",
    "            writer.add_histogram(f'grads/{name}', param.grad, global_step)  # 梯度分布"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17dd27a7",
   "metadata": {},
   "source": [
    "在 HISTOGRAMS 选项卡中查看不同层的参数分布随训练的变化。监控模型参数（如权重 weights）和梯度（grads）的分布变化，诊断训练问题（如梯度消失 / 爆炸）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2bfc460f",
   "metadata": {},
   "source": [
    "### 1.8 启动tensorboard"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "caa99c08",
   "metadata": {},
   "source": [
    "运行代码后，会在指定目录（如 runs/cifar10_mlp_experiment_1）生成 .tfevents 文件，存储所有 TensorBoard 数据。\n",
    "\n",
    "在终端执行（需进入项目根目录）：\n",
    "\n",
    "tensorboard --logdir=runs  # 假设日志目录在 runs/ 下\n",
    "\n",
    "打开浏览器，输入终端提示的 URL（通常为 http://localhost:6006）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "576c8942",
   "metadata": {},
   "source": [
    "## 二、tensorboard实战"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17a1c18e",
   "metadata": {},
   "source": [
    "### 2.1 cifar-10 MLP实战"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "45eb0ee3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Files already downloaded and verified\n",
      "开始训练模型...\n",
      "TensorBoard日志保存在: runs/cifar10_mlp_experiment_1\n",
      "训练完成后，使用命令 `tensorboard --logdir=runs` 启动TensorBoard查看可视化结果\n",
      "Epoch: 1/20 | Batch: 100/782 | 单Batch损失: 1.8327 | 累计平均损失: 1.9410\n",
      "Epoch: 1/20 | Batch: 200/782 | 单Batch损失: 1.8588 | 累计平均损失: 1.8519\n",
      "Epoch: 1/20 | Batch: 300/782 | 单Batch损失: 1.6719 | 累计平均损失: 1.8029\n",
      "Epoch: 1/20 | Batch: 400/782 | 单Batch损失: 1.7609 | 累计平均损失: 1.7754\n",
      "Epoch: 1/20 | Batch: 500/782 | 单Batch损失: 1.6642 | 累计平均损失: 1.7508\n",
      "Epoch: 1/20 | Batch: 600/782 | 单Batch损失: 1.6564 | 累计平均损失: 1.7330\n",
      "Epoch: 1/20 | Batch: 700/782 | 单Batch损失: 1.5870 | 累计平均损失: 1.7199\n",
      "Epoch 1/20 完成 | 训练准确率: 39.23% | 测试准确率: 45.11%\n",
      "Epoch: 2/20 | Batch: 100/782 | 单Batch损失: 1.4987 | 累计平均损失: 1.5227\n",
      "Epoch: 2/20 | Batch: 200/782 | 单Batch损失: 1.3297 | 累计平均损失: 1.4918\n",
      "Epoch: 2/20 | Batch: 300/782 | 单Batch损失: 1.3329 | 累计平均损失: 1.4820\n",
      "Epoch: 2/20 | Batch: 400/782 | 单Batch损失: 1.5894 | 累计平均损失: 1.4701\n",
      "Epoch: 2/20 | Batch: 500/782 | 单Batch损失: 1.3843 | 累计平均损失: 1.4710\n",
      "Epoch: 2/20 | Batch: 600/782 | 单Batch损失: 1.3671 | 累计平均损失: 1.4662\n",
      "Epoch: 2/20 | Batch: 700/782 | 单Batch损失: 1.4408 | 累计平均损失: 1.4614\n",
      "Epoch 2/20 完成 | 训练准确率: 48.51% | 测试准确率: 49.87%\n",
      "Epoch: 3/20 | Batch: 100/782 | 单Batch损失: 1.3722 | 累计平均损失: 1.3401\n",
      "Epoch: 3/20 | Batch: 200/782 | 单Batch损失: 1.8139 | 累计平均损失: 1.3486\n",
      "Epoch: 3/20 | Batch: 300/782 | 单Batch损失: 1.1994 | 累计平均损失: 1.3457\n",
      "Epoch: 3/20 | Batch: 400/782 | 单Batch损失: 1.1896 | 累计平均损失: 1.3403\n",
      "Epoch: 3/20 | Batch: 500/782 | 单Batch损失: 1.4191 | 累计平均损失: 1.3419\n",
      "Epoch: 3/20 | Batch: 600/782 | 单Batch损失: 1.4218 | 累计平均损失: 1.3475\n",
      "Epoch: 3/20 | Batch: 700/782 | 单Batch损失: 1.4627 | 累计平均损失: 1.3441\n",
      "Epoch 3/20 完成 | 训练准确率: 52.43% | 测试准确率: 51.27%\n",
      "Epoch: 4/20 | Batch: 100/782 | 单Batch损失: 1.3596 | 累计平均损失: 1.2346\n",
      "Epoch: 4/20 | Batch: 200/782 | 单Batch损失: 1.3270 | 累计平均损失: 1.2381\n",
      "Epoch: 4/20 | Batch: 300/782 | 单Batch损失: 1.2478 | 累计平均损失: 1.2434\n",
      "Epoch: 4/20 | Batch: 400/782 | 单Batch损失: 1.3861 | 累计平均损失: 1.2422\n",
      "Epoch: 4/20 | Batch: 500/782 | 单Batch损失: 1.3478 | 累计平均损失: 1.2422\n",
      "Epoch: 4/20 | Batch: 600/782 | 单Batch损失: 1.1521 | 累计平均损失: 1.2447\n",
      "Epoch: 4/20 | Batch: 700/782 | 单Batch损失: 1.2833 | 累计平均损失: 1.2469\n",
      "Epoch 4/20 完成 | 训练准确率: 55.63% | 测试准确率: 51.32%\n",
      "Epoch: 5/20 | Batch: 100/782 | 单Batch损失: 0.9809 | 累计平均损失: 1.1235\n",
      "Epoch: 5/20 | Batch: 200/782 | 单Batch损失: 1.0800 | 累计平均损失: 1.1295\n",
      "Epoch: 5/20 | Batch: 300/782 | 单Batch损失: 1.0129 | 累计平均损失: 1.1372\n",
      "Epoch: 5/20 | Batch: 400/782 | 单Batch损失: 1.0918 | 累计平均损失: 1.1459\n",
      "Epoch: 5/20 | Batch: 500/782 | 单Batch损失: 1.3155 | 累计平均损失: 1.1532\n",
      "Epoch: 5/20 | Batch: 600/782 | 单Batch损失: 1.1727 | 累计平均损失: 1.1588\n",
      "Epoch: 5/20 | Batch: 700/782 | 单Batch损失: 1.2888 | 累计平均损失: 1.1649\n",
      "Epoch 5/20 完成 | 训练准确率: 58.74% | 测试准确率: 52.74%\n",
      "Epoch: 6/20 | Batch: 100/782 | 单Batch损失: 1.1855 | 累计平均损失: 1.0499\n",
      "Epoch: 6/20 | Batch: 200/782 | 单Batch损失: 0.8994 | 累计平均损失: 1.0567\n",
      "Epoch: 6/20 | Batch: 300/782 | 单Batch损失: 1.2460 | 累计平均损失: 1.0602\n",
      "Epoch: 6/20 | Batch: 400/782 | 单Batch损失: 1.1033 | 累计平均损失: 1.0660\n",
      "Epoch: 6/20 | Batch: 500/782 | 单Batch损失: 0.9182 | 累计平均损失: 1.0679\n",
      "Epoch: 6/20 | Batch: 600/782 | 单Batch损失: 1.4116 | 累计平均损失: 1.0745\n",
      "Epoch: 6/20 | Batch: 700/782 | 单Batch损失: 1.0211 | 累计平均损失: 1.0814\n",
      "Epoch 6/20 完成 | 训练准确率: 61.37% | 测试准确率: 52.98%\n",
      "Epoch: 7/20 | Batch: 100/782 | 单Batch损失: 1.0082 | 累计平均损失: 0.9592\n",
      "Epoch: 7/20 | Batch: 200/782 | 单Batch损失: 1.0255 | 累计平均损失: 0.9742\n",
      "Epoch: 7/20 | Batch: 300/782 | 单Batch损失: 1.1416 | 累计平均损失: 0.9837\n",
      "Epoch: 7/20 | Batch: 400/782 | 单Batch损失: 0.9732 | 累计平均损失: 0.9875\n",
      "Epoch: 7/20 | Batch: 500/782 | 单Batch损失: 1.1387 | 累计平均损失: 0.9947\n",
      "Epoch: 7/20 | Batch: 600/782 | 单Batch损失: 0.8657 | 累计平均损失: 0.9994\n",
      "Epoch: 7/20 | Batch: 700/782 | 单Batch损失: 0.9666 | 累计平均损失: 1.0046\n",
      "Epoch 7/20 完成 | 训练准确率: 64.09% | 测试准确率: 52.69%\n",
      "Epoch: 8/20 | Batch: 100/782 | 单Batch损失: 0.6081 | 累计平均损失: 0.8927\n",
      "Epoch: 8/20 | Batch: 200/782 | 单Batch损失: 0.6484 | 累计平均损失: 0.8922\n",
      "Epoch: 8/20 | Batch: 300/782 | 单Batch损失: 0.8360 | 累计平均损失: 0.9001\n",
      "Epoch: 8/20 | Batch: 400/782 | 单Batch损失: 1.1883 | 累计平均损失: 0.9150\n",
      "Epoch: 8/20 | Batch: 500/782 | 单Batch损失: 0.9597 | 累计平均损失: 0.9244\n",
      "Epoch: 8/20 | Batch: 600/782 | 单Batch损失: 0.8802 | 累计平均损失: 0.9273\n",
      "Epoch: 8/20 | Batch: 700/782 | 单Batch损失: 0.9168 | 累计平均损失: 0.9295\n",
      "Epoch 8/20 完成 | 训练准确率: 66.68% | 测试准确率: 52.01%\n",
      "Epoch: 9/20 | Batch: 100/782 | 单Batch损失: 0.8491 | 累计平均损失: 0.7973\n",
      "Epoch: 9/20 | Batch: 200/782 | 单Batch损失: 0.8207 | 累计平均损失: 0.8219\n",
      "Epoch: 9/20 | Batch: 300/782 | 单Batch损失: 0.9952 | 累计平均损失: 0.8260\n",
      "Epoch: 9/20 | Batch: 400/782 | 单Batch损失: 0.8664 | 累计平均损失: 0.8395\n",
      "Epoch: 9/20 | Batch: 500/782 | 单Batch损失: 0.8573 | 累计平均损失: 0.8478\n",
      "Epoch: 9/20 | Batch: 600/782 | 单Batch损失: 1.2844 | 累计平均损失: 0.8503\n",
      "Epoch: 9/20 | Batch: 700/782 | 单Batch损失: 0.7931 | 累计平均损失: 0.8556\n",
      "Epoch 9/20 完成 | 训练准确率: 69.11% | 测试准确率: 53.24%\n",
      "Epoch: 10/20 | Batch: 100/782 | 单Batch损失: 0.6661 | 累计平均损失: 0.7471\n",
      "Epoch: 10/20 | Batch: 200/782 | 单Batch损失: 0.7758 | 累计平均损失: 0.7521\n",
      "Epoch: 10/20 | Batch: 300/782 | 单Batch损失: 1.1638 | 累计平均损失: 0.7680\n",
      "Epoch: 10/20 | Batch: 400/782 | 单Batch损失: 0.7825 | 累计平均损失: 0.7754\n",
      "Epoch: 10/20 | Batch: 500/782 | 单Batch损失: 0.6984 | 累计平均损失: 0.7834\n",
      "Epoch: 10/20 | Batch: 600/782 | 单Batch损失: 0.7199 | 累计平均损失: 0.7880\n",
      "Epoch: 10/20 | Batch: 700/782 | 单Batch损失: 0.9765 | 累计平均损失: 0.7918\n",
      "Epoch 10/20 完成 | 训练准确率: 71.70% | 测试准确率: 53.59%\n",
      "Epoch: 11/20 | Batch: 100/782 | 单Batch损失: 0.7485 | 累计平均损失: 0.6873\n",
      "Epoch: 11/20 | Batch: 200/782 | 单Batch损失: 0.6853 | 累计平均损失: 0.6817\n",
      "Epoch: 11/20 | Batch: 300/782 | 单Batch损失: 0.7594 | 累计平均损失: 0.6880\n",
      "Epoch: 11/20 | Batch: 400/782 | 单Batch损失: 0.9249 | 累计平均损失: 0.7001\n",
      "Epoch: 11/20 | Batch: 500/782 | 单Batch损失: 0.5742 | 累计平均损失: 0.7060\n",
      "Epoch: 11/20 | Batch: 600/782 | 单Batch损失: 0.7716 | 累计平均损失: 0.7190\n",
      "Epoch: 11/20 | Batch: 700/782 | 单Batch损失: 0.6123 | 累计平均损失: 0.7273\n",
      "Epoch 11/20 完成 | 训练准确率: 73.83% | 测试准确率: 52.58%\n",
      "Epoch: 12/20 | Batch: 100/782 | 单Batch损失: 0.6315 | 累计平均损失: 0.6275\n",
      "Epoch: 12/20 | Batch: 200/782 | 单Batch损失: 0.5326 | 累计平均损失: 0.6286\n",
      "Epoch: 12/20 | Batch: 300/782 | 单Batch损失: 0.5623 | 累计平均损失: 0.6369\n",
      "Epoch: 12/20 | Batch: 400/782 | 单Batch损失: 0.7911 | 累计平均损失: 0.6473\n",
      "Epoch: 12/20 | Batch: 500/782 | 单Batch损失: 0.6620 | 累计平均损失: 0.6545\n",
      "Epoch: 12/20 | Batch: 600/782 | 单Batch损失: 0.5583 | 累计平均损失: 0.6637\n",
      "Epoch: 12/20 | Batch: 700/782 | 单Batch损失: 0.6010 | 累计平均损失: 0.6709\n",
      "Epoch 12/20 完成 | 训练准确率: 75.82% | 测试准确率: 52.88%\n",
      "Epoch: 13/20 | Batch: 100/782 | 单Batch损失: 0.7061 | 累计平均损失: 0.5733\n",
      "Epoch: 13/20 | Batch: 200/782 | 单Batch损失: 0.5555 | 累计平均损失: 0.5713\n",
      "Epoch: 13/20 | Batch: 300/782 | 单Batch损失: 0.3972 | 累计平均损失: 0.5712\n",
      "Epoch: 13/20 | Batch: 400/782 | 单Batch损失: 0.8246 | 累计平均损失: 0.5824\n",
      "Epoch: 13/20 | Batch: 500/782 | 单Batch损失: 0.4577 | 累计平均损失: 0.5935\n",
      "Epoch: 13/20 | Batch: 600/782 | 单Batch损失: 0.7397 | 累计平均损失: 0.5992\n",
      "Epoch: 13/20 | Batch: 700/782 | 单Batch损失: 0.6297 | 累计平均损失: 0.6090\n",
      "Epoch 13/20 完成 | 训练准确率: 78.18% | 测试准确率: 53.19%\n",
      "Epoch: 14/20 | Batch: 100/782 | 单Batch损失: 0.5944 | 累计平均损失: 0.5333\n",
      "Epoch: 14/20 | Batch: 200/782 | 单Batch损失: 0.5172 | 累计平均损失: 0.5252\n",
      "Epoch: 14/20 | Batch: 300/782 | 单Batch损失: 0.5107 | 累计平均损失: 0.5313\n",
      "Epoch: 14/20 | Batch: 400/782 | 单Batch损失: 0.4882 | 累计平均损失: 0.5414\n",
      "Epoch: 14/20 | Batch: 500/782 | 单Batch损失: 0.4880 | 累计平均损失: 0.5560\n",
      "Epoch: 14/20 | Batch: 600/782 | 单Batch损失: 0.6760 | 累计平均损失: 0.5617\n",
      "Epoch: 14/20 | Batch: 700/782 | 单Batch损失: 0.5190 | 累计平均损失: 0.5651\n",
      "Epoch 14/20 完成 | 训练准确率: 79.50% | 测试准确率: 53.00%\n",
      "Epoch: 15/20 | Batch: 100/782 | 单Batch损失: 0.3614 | 累计平均损失: 0.4667\n",
      "Epoch: 15/20 | Batch: 200/782 | 单Batch损失: 0.5322 | 累计平均损失: 0.4657\n",
      "Epoch: 15/20 | Batch: 300/782 | 单Batch损失: 0.5792 | 累计平均损失: 0.4838\n",
      "Epoch: 15/20 | Batch: 400/782 | 单Batch损失: 0.6562 | 累计平均损失: 0.4975\n",
      "Epoch: 15/20 | Batch: 500/782 | 单Batch损失: 0.5755 | 累计平均损失: 0.5062\n",
      "Epoch: 15/20 | Batch: 600/782 | 单Batch损失: 0.8258 | 累计平均损失: 0.5142\n",
      "Epoch: 15/20 | Batch: 700/782 | 单Batch损失: 0.4823 | 累计平均损失: 0.5194\n",
      "Epoch 15/20 完成 | 训练准确率: 81.21% | 测试准确率: 52.39%\n",
      "Epoch: 16/20 | Batch: 100/782 | 单Batch损失: 0.3308 | 累计平均损失: 0.4314\n",
      "Epoch: 16/20 | Batch: 200/782 | 单Batch损失: 0.3376 | 累计平均损失: 0.4463\n",
      "Epoch: 16/20 | Batch: 300/782 | 单Batch损失: 0.5752 | 累计平均损失: 0.4539\n",
      "Epoch: 16/20 | Batch: 400/782 | 单Batch损失: 0.4853 | 累计平均损失: 0.4700\n",
      "Epoch: 16/20 | Batch: 500/782 | 单Batch损失: 0.5356 | 累计平均损失: 0.4794\n",
      "Epoch: 16/20 | Batch: 600/782 | 单Batch损失: 0.6754 | 累计平均损失: 0.4817\n",
      "Epoch: 16/20 | Batch: 700/782 | 单Batch损失: 0.4735 | 累计平均损失: 0.4875\n",
      "Epoch 16/20 完成 | 训练准确率: 82.41% | 测试准确率: 53.40%\n",
      "Epoch: 17/20 | Batch: 100/782 | 单Batch损失: 0.3944 | 累计平均损失: 0.4055\n",
      "Epoch: 17/20 | Batch: 200/782 | 单Batch损失: 0.3707 | 累计平均损失: 0.4074\n",
      "Epoch: 17/20 | Batch: 300/782 | 单Batch损失: 0.5363 | 累计平均损失: 0.4122\n",
      "Epoch: 17/20 | Batch: 400/782 | 单Batch损失: 0.3647 | 累计平均损失: 0.4147\n",
      "Epoch: 17/20 | Batch: 500/782 | 单Batch损失: 0.4949 | 累计平均损失: 0.4241\n",
      "Epoch: 17/20 | Batch: 600/782 | 单Batch损失: 0.2563 | 累计平均损失: 0.4316\n",
      "Epoch: 17/20 | Batch: 700/782 | 单Batch损失: 0.3814 | 累计平均损失: 0.4394\n",
      "Epoch 17/20 完成 | 训练准确率: 84.10% | 测试准确率: 51.73%\n",
      "Epoch: 18/20 | Batch: 100/782 | 单Batch损失: 0.4645 | 累计平均损失: 0.3851\n",
      "Epoch: 18/20 | Batch: 200/782 | 单Batch损失: 0.2752 | 累计平均损失: 0.3906\n",
      "Epoch: 18/20 | Batch: 300/782 | 单Batch损失: 0.4404 | 累计平均损失: 0.3927\n",
      "Epoch: 18/20 | Batch: 400/782 | 单Batch损失: 0.4450 | 累计平均损失: 0.4015\n",
      "Epoch: 18/20 | Batch: 500/782 | 单Batch损失: 0.4082 | 累计平均损失: 0.4158\n",
      "Epoch: 18/20 | Batch: 600/782 | 单Batch损失: 0.3982 | 累计平均损失: 0.4203\n",
      "Epoch: 18/20 | Batch: 700/782 | 单Batch损失: 0.5168 | 累计平均损失: 0.4263\n",
      "Epoch 18/20 完成 | 训练准确率: 84.83% | 测试准确率: 51.31%\n",
      "Epoch: 19/20 | Batch: 100/782 | 单Batch损失: 0.2534 | 累计平均损失: 0.3471\n",
      "Epoch: 19/20 | Batch: 200/782 | 单Batch损失: 0.3286 | 累计平均损失: 0.3488\n",
      "Epoch: 19/20 | Batch: 300/782 | 单Batch损失: 0.2713 | 累计平均损失: 0.3563\n",
      "Epoch: 19/20 | Batch: 400/782 | 单Batch损失: 0.4733 | 累计平均损失: 0.3728\n",
      "Epoch: 19/20 | Batch: 500/782 | 单Batch损失: 0.3166 | 累计平均损失: 0.3756\n",
      "Epoch: 19/20 | Batch: 600/782 | 单Batch损失: 0.4382 | 累计平均损失: 0.3798\n",
      "Epoch: 19/20 | Batch: 700/782 | 单Batch损失: 0.3680 | 累计平均损失: 0.3888\n",
      "Epoch 19/20 完成 | 训练准确率: 86.05% | 测试准确率: 52.17%\n",
      "Epoch: 20/20 | Batch: 100/782 | 单Batch损失: 0.2334 | 累计平均损失: 0.3316\n",
      "Epoch: 20/20 | Batch: 200/782 | 单Batch损失: 0.3335 | 累计平均损失: 0.3274\n",
      "Epoch: 20/20 | Batch: 300/782 | 单Batch损失: 0.4049 | 累计平均损失: 0.3455\n",
      "Epoch: 20/20 | Batch: 400/782 | 单Batch损失: 0.5196 | 累计平均损失: 0.3514\n",
      "Epoch: 20/20 | Batch: 500/782 | 单Batch损失: 0.3912 | 累计平均损失: 0.3619\n",
      "Epoch: 20/20 | Batch: 600/782 | 单Batch损失: 0.2988 | 累计平均损失: 0.3776\n",
      "Epoch: 20/20 | Batch: 700/782 | 单Batch损失: 0.5925 | 累计平均损失: 0.3867\n",
      "Epoch 20/20 完成 | 训练准确率: 86.26% | 测试准确率: 51.46%\n",
      "训练完成！最终测试准确率: 51.46%\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "import torchvision\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "from torch.utils.tensorboard import SummaryWriter\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import os\n",
    "\n",
    "# 设置随机种子以确保结果可复现\n",
    "torch.manual_seed(42)\n",
    "np.random.seed(42)\n",
    "\n",
    "# 1. 数据预处理\n",
    "transform = transforms.Compose([\n",
    "    transforms.ToTensor(),                # 转换为张量\n",
    "    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 标准化处理\n",
    "])\n",
    "\n",
    "# 2. 加载CIFAR-10数据集\n",
    "train_dataset = datasets.CIFAR10(\n",
    "    root='./data',\n",
    "    train=True,\n",
    "    download=True,\n",
    "    transform=transform\n",
    ")\n",
    "\n",
    "test_dataset = datasets.CIFAR10(\n",
    "    root='./data',\n",
    "    train=False,\n",
    "    transform=transform\n",
    ")\n",
    "\n",
    "# 3. 创建数据加载器\n",
    "batch_size = 64\n",
    "train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)\n",
    "\n",
    "# CIFAR-10的类别名称\n",
    "classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')\n",
    "\n",
    "# 4. 定义MLP模型（适应CIFAR-10的输入尺寸）\n",
    "class MLP(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(MLP, self).__init__()\n",
    "        self.flatten = nn.Flatten()  # 将3x32x32的图像展平为3072维向量\n",
    "        self.layer1 = nn.Linear(3072, 512)  # 第一层：3072个输入，512个神经元\n",
    "        self.relu1 = nn.ReLU()\n",
    "        self.dropout1 = nn.Dropout(0.2)  # 添加Dropout防止过拟合\n",
    "        self.layer2 = nn.Linear(512, 256)  # 第二层：512个输入，256个神经元\n",
    "        self.relu2 = nn.ReLU()\n",
    "        self.dropout2 = nn.Dropout(0.2)\n",
    "        self.layer3 = nn.Linear(256, 10)  # 输出层：10个类别\n",
    "        \n",
    "    def forward(self, x):\n",
    "        # 第一步：将输入图像展平为一维向量\n",
    "        x = self.flatten(x)  # 输入尺寸: [batch_size, 3, 32, 32] → [batch_size, 3072]\n",
    "        \n",
    "        # 第一层全连接 + 激活 + Dropout\n",
    "        x = self.layer1(x)   # 线性变换: [batch_size, 3072] → [batch_size, 512]\n",
    "        x = self.relu1(x)    # 应用ReLU激活函数\n",
    "        x = self.dropout1(x) # 训练时随机丢弃部分神经元输出\n",
    "        \n",
    "        # 第二层全连接 + 激活 + Dropout\n",
    "        x = self.layer2(x)   # 线性变换: [batch_size, 512] → [batch_size, 256]\n",
    "        x = self.relu2(x)    # 应用ReLU激活函数\n",
    "        x = self.dropout2(x) # 训练时随机丢弃部分神经元输出\n",
    "        \n",
    "        # 第三层（输出层）全连接\n",
    "        x = self.layer3(x)   # 线性变换: [batch_size, 256] → [batch_size, 10]\n",
    "        \n",
    "        return x  # 返回未经过Softmax的logits\n",
    "\n",
    "# 检查GPU是否可用\n",
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "\n",
    "# 初始化模型\n",
    "model = MLP()\n",
    "model = model.to(device)  # 将模型移至GPU（如果可用）\n",
    "\n",
    "criterion = nn.CrossEntropyLoss()  # 交叉熵损失函数\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.001)  # Adam优化器\n",
    "\n",
    "# 创建TensorBoard的SummaryWriter，指定日志保存目录\n",
    "log_dir = 'runs/cifar10_mlp_experiment'\n",
    "# 如果目录已存在，添加后缀避免覆盖\n",
    "if os.path.exists(log_dir):\n",
    "    i = 1\n",
    "    while os.path.exists(f\"{log_dir}_{i}\"):\n",
    "        i += 1\n",
    "    log_dir = f\"{log_dir}_{i}\"\n",
    "writer = SummaryWriter(log_dir)\n",
    "\n",
    "# 5. 训练模型（使用TensorBoard记录各种信息）\n",
    "def train(model, train_loader, test_loader, criterion, optimizer, device, epochs, writer):\n",
    "    model.train()  # 设置为训练模式\n",
    "    \n",
    "    # 记录训练开始时间，用于计算训练速度\n",
    "    global_step = 0\n",
    "    \n",
    "    # 可视化模型结构\n",
    "    dataiter = iter(train_loader)\n",
    "    images, labels = next(dataiter)\n",
    "    images = images.to(device)\n",
    "    writer.add_graph(model, images)  # 添加模型图\n",
    "    \n",
    "    # 可视化原始图像样本\n",
    "    img_grid = torchvision.utils.make_grid(images[:8].cpu())\n",
    "    writer.add_image('原始训练图像', img_grid)\n",
    "    \n",
    "    for epoch in range(epochs):\n",
    "        running_loss = 0.0\n",
    "        correct = 0\n",
    "        total = 0\n",
    "        \n",
    "        for batch_idx, (data, target) in enumerate(train_loader):\n",
    "            data, target = data.to(device), target.to(device)  # 移至GPU\n",
    "            \n",
    "            optimizer.zero_grad()  # 梯度清零\n",
    "            output = model(data)  # 前向传播\n",
    "            loss = criterion(output, target)  # 计算损失\n",
    "            loss.backward()  # 反向传播\n",
    "            optimizer.step()  # 更新参数\n",
    "            \n",
    "            # 统计准确率和损失\n",
    "            running_loss += loss.item()\n",
    "            _, predicted = output.max(1)\n",
    "            total += target.size(0)\n",
    "            correct += predicted.eq(target).sum().item()\n",
    "            \n",
    "            # 每100个批次记录一次信息到TensorBoard\n",
    "            if (batch_idx + 1) % 100 == 0:\n",
    "                batch_loss = loss.item()\n",
    "                batch_acc = 100. * correct / total\n",
    "                \n",
    "                # 记录标量数据（损失、准确率）\n",
    "                writer.add_scalar('Train/Batch_Loss', batch_loss, global_step)\n",
    "                writer.add_scalar('Train/Batch_Accuracy', batch_acc, global_step)\n",
    "                \n",
    "                # 记录学习率\n",
    "                writer.add_scalar('Train/Learning_Rate', optimizer.param_groups[0]['lr'], global_step)\n",
    "                \n",
    "                # 每500个批次记录一次直方图（权重和梯度）\n",
    "                if (batch_idx + 1) % 500 == 0:\n",
    "                    for name, param in model.named_parameters():\n",
    "                        writer.add_histogram(f'weights/{name}', param, global_step)\n",
    "                        if param.grad is not None:\n",
    "                            writer.add_histogram(f'grads/{name}', param.grad, global_step)\n",
    "                \n",
    "                print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} '\n",
    "                      f'| 单Batch损失: {batch_loss:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}')\n",
    "            \n",
    "            global_step += 1\n",
    "        \n",
    "        # 计算当前epoch的平均训练损失和准确率\n",
    "        epoch_train_loss = running_loss / len(train_loader)\n",
    "        epoch_train_acc = 100. * correct / total\n",
    "        \n",
    "        # 记录每个epoch的训练损失和准确率\n",
    "        writer.add_scalar('Train/Epoch_Loss', epoch_train_loss, epoch)\n",
    "        writer.add_scalar('Train/Epoch_Accuracy', epoch_train_acc, epoch)\n",
    "        \n",
    "        # 测试阶段\n",
    "        model.eval()  # 设置为评估模式\n",
    "        test_loss = 0\n",
    "        correct_test = 0\n",
    "        total_test = 0\n",
    "        \n",
    "        # 用于存储预测错误的样本\n",
    "        wrong_images = []\n",
    "        wrong_labels = []\n",
    "        wrong_preds = []\n",
    "        \n",
    "        with torch.no_grad():\n",
    "            for data, target in test_loader:\n",
    "                data, target = data.to(device), target.to(device)\n",
    "                output = model(data)\n",
    "                test_loss += criterion(output, target).item()\n",
    "                _, predicted = output.max(1)\n",
    "                total_test += target.size(0)\n",
    "                correct_test += predicted.eq(target).sum().item()\n",
    "                \n",
    "                # 收集预测错误的样本\n",
    "                wrong_mask = (predicted != target).cpu()\n",
    "                if wrong_mask.sum() > 0:\n",
    "                    wrong_batch_images = data[wrong_mask].cpu()\n",
    "                    wrong_batch_labels = target[wrong_mask].cpu()\n",
    "                    wrong_batch_preds = predicted[wrong_mask].cpu()\n",
    "                    \n",
    "                    wrong_images.extend(wrong_batch_images)\n",
    "                    wrong_labels.extend(wrong_batch_labels)\n",
    "                    wrong_preds.extend(wrong_batch_preds)\n",
    "        \n",
    "        epoch_test_loss = test_loss / len(test_loader)\n",
    "        epoch_test_acc = 100. * correct_test / total_test\n",
    "        \n",
    "        # 记录每个epoch的测试损失和准确率\n",
    "        writer.add_scalar('Test/Loss', epoch_test_loss, epoch)\n",
    "        writer.add_scalar('Test/Accuracy', epoch_test_acc, epoch)\n",
    "        \n",
    "        # 计算并记录训练速度（每秒处理的样本数）\n",
    "        # 这里简化处理，假设每个epoch的时间相同\n",
    "        samples_per_epoch = len(train_loader.dataset)\n",
    "        # 实际应用中应该使用time.time()来计算真实时间\n",
    "        \n",
    "        print(f'Epoch {epoch+1}/{epochs} 完成 | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%')\n",
    "        \n",
    "        # 可视化预测错误的样本（只在最后一个epoch进行）\n",
    "        if epoch == epochs - 1 and len(wrong_images) > 0:\n",
    "            # 最多显示8个错误样本\n",
    "            display_count = min(8, len(wrong_images))\n",
    "            wrong_img_grid = torchvision.utils.make_grid(wrong_images[:display_count])\n",
    "            \n",
    "            # 创建错误预测的标签文本\n",
    "            wrong_text = []\n",
    "            for i in range(display_count):\n",
    "                true_label = classes[wrong_labels[i]]\n",
    "                pred_label = classes[wrong_preds[i]]\n",
    "                wrong_text.append(f'True: {true_label}, Pred: {pred_label}')\n",
    "            \n",
    "            writer.add_image('错误预测样本', wrong_img_grid)\n",
    "            writer.add_text('错误预测标签', '\\n'.join(wrong_text), epoch)\n",
    "    \n",
    "    # 关闭TensorBoard写入器\n",
    "    writer.close()\n",
    "    \n",
    "    return epoch_test_acc  # 返回最终测试准确率\n",
    "\n",
    "# 6. 执行训练和测试\n",
    "epochs = 20  # 训练轮次\n",
    "print(\"开始训练模型...\")\n",
    "print(f\"TensorBoard日志保存在: {log_dir}\")\n",
    "print(\"训练完成后，使用命令 `tensorboard --logdir=runs` 启动TensorBoard查看可视化结果\")\n",
    "\n",
    "final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, device, epochs, writer)\n",
    "print(f\"训练完成！最终测试准确率: {final_accuracy:.2f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07ef2f2b",
   "metadata": {},
   "source": [
    "TensorBoard日志保存在: runs/cifar10_mlp_experiment_1\n",
    "可以在命令行中进入目前的环境，然后通过tensorboard --logdir=xxxx（目录）即可调出本地链接，点进去就是目前的训练信息，可以不断F5刷新来查看变化。\n",
    "\n",
    "\n",
    "\n",
    "在TensorBoard界面中，你可以看到：\n",
    "\n",
    "1. SCALARS 选项卡：展示损失曲线、准确率变化、学习率等标量数据----Scalar意思是标量，指只有大小、没有方向的量。\n",
    "\n",
    "2. IMAGES 选项卡：展示原始训练图像和错误预测的样本\n",
    "\n",
    "3. GRAPHS 选项卡：展示模型的计算图结构\n",
    "\n",
    "4. HISTOGRAMS 选项卡：展示模型参数和梯度的分布直方图"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "936e1933",
   "metadata": {},
   "source": [
    "### 2.2 cifar-10 CNN实战 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "e9a1795b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "使用设备: cuda\n",
      "Files already downloaded and verified\n",
      "开始使用CNN训练模型...\n",
      "TensorBoard 日志目录: runs/cifar10_cnn_exp\n",
      "训练后执行: tensorboard --logdir=runs 查看可视化\n",
      "Epoch: 1/20 | Batch: 100/782 | 单Batch损失: 1.8809 | 累计平均损失: 2.0134\n",
      "Epoch: 1/20 | Batch: 200/782 | 单Batch损失: 1.7645 | 累计平均损失: 1.8838\n",
      "Epoch: 1/20 | Batch: 300/782 | 单Batch损失: 1.6334 | 累计平均损失: 1.8246\n",
      "Epoch: 1/20 | Batch: 400/782 | 单Batch损失: 1.6380 | 累计平均损失: 1.7784\n",
      "Epoch: 1/20 | Batch: 500/782 | 单Batch损失: 1.5500 | 累计平均损失: 1.7435\n",
      "Epoch: 1/20 | Batch: 600/782 | 单Batch损失: 1.5527 | 累计平均损失: 1.7107\n",
      "Epoch: 1/20 | Batch: 700/782 | 单Batch损失: 1.4984 | 累计平均损失: 1.6852\n",
      "Epoch 1/20 完成 | 训练准确率: 38.11% | 测试准确率: 52.47%\n",
      "Epoch: 2/20 | Batch: 100/782 | 单Batch损失: 1.3814 | 累计平均损失: 1.4373\n",
      "Epoch: 2/20 | Batch: 200/782 | 单Batch损失: 1.2911 | 累计平均损失: 1.3985\n",
      "Epoch: 2/20 | Batch: 300/782 | 单Batch损失: 1.1904 | 累计平均损失: 1.3747\n",
      "Epoch: 2/20 | Batch: 400/782 | 单Batch损失: 1.4026 | 累计平均损失: 1.3556\n",
      "Epoch: 2/20 | Batch: 500/782 | 单Batch损失: 1.0859 | 累计平均损失: 1.3323\n",
      "Epoch: 2/20 | Batch: 600/782 | 单Batch损失: 1.0579 | 累计平均损失: 1.3118\n",
      "Epoch: 2/20 | Batch: 700/782 | 单Batch损失: 1.0614 | 累计平均损失: 1.2968\n",
      "Epoch 2/20 完成 | 训练准确率: 53.28% | 测试准确率: 64.18%\n",
      "Epoch: 3/20 | Batch: 100/782 | 单Batch损失: 1.2669 | 累计平均损失: 1.1595\n",
      "Epoch: 3/20 | Batch: 200/782 | 单Batch损失: 1.1426 | 累计平均损失: 1.1423\n",
      "Epoch: 3/20 | Batch: 300/782 | 单Batch损失: 0.9215 | 累计平均损失: 1.1318\n",
      "Epoch: 3/20 | Batch: 400/782 | 单Batch损失: 0.9795 | 累计平均损失: 1.1233\n",
      "Epoch: 3/20 | Batch: 500/782 | 单Batch损失: 1.2100 | 累计平均损失: 1.1204\n",
      "Epoch: 3/20 | Batch: 600/782 | 单Batch损失: 1.1693 | 累计平均损失: 1.1098\n",
      "Epoch: 3/20 | Batch: 700/782 | 单Batch损失: 1.0973 | 累计平均损失: 1.1007\n",
      "Epoch 3/20 完成 | 训练准确率: 61.37% | 测试准确率: 67.55%\n",
      "Epoch: 4/20 | Batch: 100/782 | 单Batch损失: 0.8795 | 累计平均损失: 1.0080\n",
      "Epoch: 4/20 | Batch: 200/782 | 单Batch损失: 1.0070 | 累计平均损失: 1.0122\n",
      "Epoch: 4/20 | Batch: 300/782 | 单Batch损失: 1.1206 | 累计平均损失: 1.0071\n",
      "Epoch: 4/20 | Batch: 400/782 | 单Batch损失: 1.0918 | 累计平均损失: 1.0017\n",
      "Epoch: 4/20 | Batch: 500/782 | 单Batch损失: 0.8132 | 累计平均损失: 0.9982\n",
      "Epoch: 4/20 | Batch: 600/782 | 单Batch损失: 1.1464 | 累计平均损失: 0.9895\n",
      "Epoch: 4/20 | Batch: 700/782 | 单Batch损失: 0.9950 | 累计平均损失: 0.9883\n",
      "Epoch 4/20 完成 | 训练准确率: 65.12% | 测试准确率: 70.56%\n",
      "Epoch: 5/20 | Batch: 100/782 | 单Batch损失: 0.9320 | 累计平均损失: 0.9707\n",
      "Epoch: 5/20 | Batch: 200/782 | 单Batch损失: 0.9041 | 累计平均损失: 0.9490\n",
      "Epoch: 5/20 | Batch: 300/782 | 单Batch损失: 0.7707 | 累计平均损失: 0.9494\n",
      "Epoch: 5/20 | Batch: 400/782 | 单Batch损失: 0.8947 | 累计平均损失: 0.9423\n",
      "Epoch: 5/20 | Batch: 500/782 | 单Batch损失: 0.8728 | 累计平均损失: 0.9352\n",
      "Epoch: 5/20 | Batch: 600/782 | 单Batch损失: 0.9779 | 累计平均损失: 0.9290\n",
      "Epoch: 5/20 | Batch: 700/782 | 单Batch损失: 0.9652 | 累计平均损失: 0.9266\n",
      "Epoch 5/20 完成 | 训练准确率: 67.76% | 测试准确率: 74.09%\n",
      "Epoch: 6/20 | Batch: 100/782 | 单Batch损失: 0.8804 | 累计平均损失: 0.8748\n",
      "Epoch: 6/20 | Batch: 200/782 | 单Batch损失: 0.9413 | 累计平均损失: 0.8779\n",
      "Epoch: 6/20 | Batch: 300/782 | 单Batch损失: 0.9451 | 累计平均损失: 0.8813\n",
      "Epoch: 6/20 | Batch: 400/782 | 单Batch损失: 0.9844 | 累计平均损失: 0.8811\n",
      "Epoch: 6/20 | Batch: 500/782 | 单Batch损失: 0.9123 | 累计平均损失: 0.8804\n",
      "Epoch: 6/20 | Batch: 600/782 | 单Batch损失: 0.7724 | 累计平均损失: 0.8747\n",
      "Epoch: 6/20 | Batch: 700/782 | 单Batch损失: 0.9191 | 累计平均损失: 0.8738\n",
      "Epoch 6/20 完成 | 训练准确率: 69.24% | 测试准确率: 74.44%\n",
      "Epoch: 7/20 | Batch: 100/782 | 单Batch损失: 0.4618 | 累计平均损失: 0.8522\n",
      "Epoch: 7/20 | Batch: 200/782 | 单Batch损失: 1.0956 | 累计平均损失: 0.8398\n",
      "Epoch: 7/20 | Batch: 300/782 | 单Batch损失: 0.7080 | 累计平均损失: 0.8442\n",
      "Epoch: 7/20 | Batch: 400/782 | 单Batch损失: 0.8755 | 累计平均损失: 0.8423\n",
      "Epoch: 7/20 | Batch: 500/782 | 单Batch损失: 1.0161 | 累计平均损失: 0.8451\n",
      "Epoch: 7/20 | Batch: 600/782 | 单Batch损失: 0.9611 | 累计平均损失: 0.8436\n",
      "Epoch: 7/20 | Batch: 700/782 | 单Batch损失: 0.9344 | 累计平均损失: 0.8433\n",
      "Epoch 7/20 完成 | 训练准确率: 70.60% | 测试准确率: 75.97%\n",
      "Epoch: 8/20 | Batch: 100/782 | 单Batch损失: 0.5846 | 累计平均损失: 0.7982\n",
      "Epoch: 8/20 | Batch: 200/782 | 单Batch损失: 1.1336 | 累计平均损失: 0.8046\n",
      "Epoch: 8/20 | Batch: 300/782 | 单Batch损失: 0.7393 | 累计平均损失: 0.8122\n",
      "Epoch: 8/20 | Batch: 400/782 | 单Batch损失: 0.8892 | 累计平均损失: 0.8108\n",
      "Epoch: 8/20 | Batch: 500/782 | 单Batch损失: 0.9932 | 累计平均损失: 0.8128\n",
      "Epoch: 8/20 | Batch: 600/782 | 单Batch损失: 0.8610 | 累计平均损失: 0.8154\n",
      "Epoch: 8/20 | Batch: 700/782 | 单Batch损失: 1.0081 | 累计平均损失: 0.8130\n",
      "Epoch 8/20 完成 | 训练准确率: 71.44% | 测试准确率: 76.04%\n",
      "Epoch: 9/20 | Batch: 100/782 | 单Batch损失: 0.8448 | 累计平均损失: 0.8206\n",
      "Epoch: 9/20 | Batch: 200/782 | 单Batch损失: 0.6494 | 累计平均损失: 0.8086\n",
      "Epoch: 9/20 | Batch: 300/782 | 单Batch损失: 0.8203 | 累计平均损失: 0.8021\n",
      "Epoch: 9/20 | Batch: 400/782 | 单Batch损失: 0.6053 | 累计平均损失: 0.7929\n",
      "Epoch: 9/20 | Batch: 500/782 | 单Batch损失: 0.8298 | 累计平均损失: 0.7890\n",
      "Epoch: 9/20 | Batch: 600/782 | 单Batch损失: 0.9492 | 累计平均损失: 0.7873\n",
      "Epoch: 9/20 | Batch: 700/782 | 单Batch损失: 0.7991 | 累计平均损失: 0.7889\n",
      "Epoch 9/20 完成 | 训练准确率: 72.75% | 测试准确率: 77.43%\n",
      "Epoch: 10/20 | Batch: 100/782 | 单Batch损失: 0.7773 | 累计平均损失: 0.7684\n",
      "Epoch: 10/20 | Batch: 200/782 | 单Batch损失: 0.7030 | 累计平均损失: 0.7681\n",
      "Epoch: 10/20 | Batch: 300/782 | 单Batch损失: 0.7726 | 累计平均损失: 0.7708\n",
      "Epoch: 10/20 | Batch: 400/782 | 单Batch损失: 0.7785 | 累计平均损失: 0.7681\n",
      "Epoch: 10/20 | Batch: 500/782 | 单Batch损失: 0.8096 | 累计平均损失: 0.7653\n",
      "Epoch: 10/20 | Batch: 600/782 | 单Batch损失: 0.6069 | 累计平均损失: 0.7635\n",
      "Epoch: 10/20 | Batch: 700/782 | 单Batch损失: 0.5608 | 累计平均损失: 0.7630\n",
      "Epoch 10/20 完成 | 训练准确率: 73.20% | 测试准确率: 76.64%\n",
      "Epoch: 11/20 | Batch: 100/782 | 单Batch损失: 0.7491 | 累计平均损失: 0.7709\n",
      "Epoch: 11/20 | Batch: 200/782 | 单Batch损失: 0.8199 | 累计平均损失: 0.7523\n",
      "Epoch: 11/20 | Batch: 300/782 | 单Batch损失: 1.0428 | 累计平均损失: 0.7427\n",
      "Epoch: 11/20 | Batch: 400/782 | 单Batch损失: 0.7862 | 累计平均损失: 0.7416\n",
      "Epoch: 11/20 | Batch: 500/782 | 单Batch损失: 0.7416 | 累计平均损失: 0.7450\n",
      "Epoch: 11/20 | Batch: 600/782 | 单Batch损失: 0.8239 | 累计平均损失: 0.7390\n",
      "Epoch: 11/20 | Batch: 700/782 | 单Batch损失: 0.5744 | 累计平均损失: 0.7427\n",
      "Epoch 11/20 完成 | 训练准确率: 74.04% | 测试准确率: 77.92%\n",
      "Epoch: 12/20 | Batch: 100/782 | 单Batch损失: 0.7772 | 累计平均损失: 0.7281\n",
      "Epoch: 12/20 | Batch: 200/782 | 单Batch损失: 0.6939 | 累计平均损失: 0.7296\n",
      "Epoch: 12/20 | Batch: 300/782 | 单Batch损失: 0.6478 | 累计平均损失: 0.7348\n",
      "Epoch: 12/20 | Batch: 400/782 | 单Batch损失: 0.6809 | 累计平均损失: 0.7306\n",
      "Epoch: 12/20 | Batch: 500/782 | 单Batch损失: 0.7887 | 累计平均损失: 0.7308\n",
      "Epoch: 12/20 | Batch: 600/782 | 单Batch损失: 0.9312 | 累计平均损失: 0.7293\n",
      "Epoch: 12/20 | Batch: 700/782 | 单Batch损失: 0.8912 | 累计平均损失: 0.7249\n",
      "Epoch 12/20 完成 | 训练准确率: 74.57% | 测试准确率: 78.34%\n",
      "Epoch: 13/20 | Batch: 100/782 | 单Batch损失: 0.7660 | 累计平均损失: 0.7202\n",
      "Epoch: 13/20 | Batch: 200/782 | 单Batch损失: 0.8096 | 累计平均损失: 0.7066\n",
      "Epoch: 13/20 | Batch: 300/782 | 单Batch损失: 0.6760 | 累计平均损失: 0.7041\n",
      "Epoch: 13/20 | Batch: 400/782 | 单Batch损失: 0.9175 | 累计平均损失: 0.7036\n",
      "Epoch: 13/20 | Batch: 500/782 | 单Batch损失: 0.7499 | 累计平均损失: 0.7067\n",
      "Epoch: 13/20 | Batch: 600/782 | 单Batch损失: 0.5950 | 累计平均损失: 0.7061\n",
      "Epoch: 13/20 | Batch: 700/782 | 单Batch损失: 0.5243 | 累计平均损失: 0.7101\n",
      "Epoch 13/20 完成 | 训练准确率: 75.14% | 测试准确率: 78.82%\n",
      "Epoch: 14/20 | Batch: 100/782 | 单Batch损失: 0.4825 | 累计平均损失: 0.6837\n",
      "Epoch: 14/20 | Batch: 200/782 | 单Batch损失: 0.6175 | 累计平均损失: 0.6888\n",
      "Epoch: 14/20 | Batch: 300/782 | 单Batch损失: 0.7952 | 累计平均损失: 0.6866\n",
      "Epoch: 14/20 | Batch: 400/782 | 单Batch损失: 0.5896 | 累计平均损失: 0.6942\n",
      "Epoch: 14/20 | Batch: 500/782 | 单Batch损失: 0.6090 | 累计平均损失: 0.6938\n",
      "Epoch: 14/20 | Batch: 600/782 | 单Batch损失: 0.7104 | 累计平均损失: 0.6953\n",
      "Epoch: 14/20 | Batch: 700/782 | 单Batch损失: 0.4085 | 累计平均损失: 0.6972\n",
      "Epoch 14/20 完成 | 训练准确率: 75.52% | 测试准确率: 78.96%\n",
      "Epoch: 15/20 | Batch: 100/782 | 单Batch损失: 0.6176 | 累计平均损失: 0.6590\n",
      "Epoch: 15/20 | Batch: 200/782 | 单Batch损失: 0.4711 | 累计平均损失: 0.6702\n",
      "Epoch: 15/20 | Batch: 300/782 | 单Batch损失: 0.6192 | 累计平均损失: 0.6718\n",
      "Epoch: 15/20 | Batch: 400/782 | 单Batch损失: 0.9684 | 累计平均损失: 0.6750\n",
      "Epoch: 15/20 | Batch: 500/782 | 单Batch损失: 0.5928 | 累计平均损失: 0.6773\n",
      "Epoch: 15/20 | Batch: 600/782 | 单Batch损失: 0.7290 | 累计平均损失: 0.6795\n",
      "Epoch: 15/20 | Batch: 700/782 | 单Batch损失: 0.6996 | 累计平均损失: 0.6792\n",
      "Epoch 15/20 完成 | 训练准确率: 76.17% | 测试准确率: 79.87%\n",
      "Epoch: 16/20 | Batch: 100/782 | 单Batch损失: 0.4865 | 累计平均损失: 0.6782\n",
      "Epoch: 16/20 | Batch: 200/782 | 单Batch损失: 0.5581 | 累计平均损失: 0.6671\n",
      "Epoch: 16/20 | Batch: 300/782 | 单Batch损失: 0.6508 | 累计平均损失: 0.6648\n",
      "Epoch: 16/20 | Batch: 400/782 | 单Batch损失: 0.8125 | 累计平均损失: 0.6715\n",
      "Epoch: 16/20 | Batch: 500/782 | 单Batch损失: 0.5303 | 累计平均损失: 0.6735\n",
      "Epoch: 16/20 | Batch: 600/782 | 单Batch损失: 0.6881 | 累计平均损失: 0.6737\n",
      "Epoch: 16/20 | Batch: 700/782 | 单Batch损失: 0.9869 | 累计平均损失: 0.6726\n",
      "Epoch 16/20 完成 | 训练准确率: 76.63% | 测试准确率: 79.73%\n",
      "Epoch: 17/20 | Batch: 100/782 | 单Batch损失: 0.5943 | 累计平均损失: 0.6603\n",
      "Epoch: 17/20 | Batch: 200/782 | 单Batch损失: 0.8486 | 累计平均损失: 0.6590\n",
      "Epoch: 17/20 | Batch: 300/782 | 单Batch损失: 0.5727 | 累计平均损失: 0.6586\n",
      "Epoch: 17/20 | Batch: 400/782 | 单Batch损失: 0.6489 | 累计平均损失: 0.6592\n",
      "Epoch: 17/20 | Batch: 500/782 | 单Batch损失: 0.7211 | 累计平均损失: 0.6612\n",
      "Epoch: 17/20 | Batch: 600/782 | 单Batch损失: 0.5552 | 累计平均损失: 0.6615\n",
      "Epoch: 17/20 | Batch: 700/782 | 单Batch损失: 0.5500 | 累计平均损失: 0.6617\n",
      "Epoch 17/20 完成 | 训练准确率: 76.85% | 测试准确率: 80.07%\n",
      "Epoch: 18/20 | Batch: 100/782 | 单Batch损失: 0.6643 | 累计平均损失: 0.6195\n",
      "Epoch: 18/20 | Batch: 200/782 | 单Batch损失: 0.5175 | 累计平均损失: 0.6383\n",
      "Epoch: 18/20 | Batch: 300/782 | 单Batch损失: 0.8941 | 累计平均损失: 0.6423\n",
      "Epoch: 18/20 | Batch: 400/782 | 单Batch损失: 0.5957 | 累计平均损失: 0.6494\n",
      "Epoch: 18/20 | Batch: 500/782 | 单Batch损失: 0.6997 | 累计平均损失: 0.6515\n",
      "Epoch: 18/20 | Batch: 600/782 | 单Batch损失: 0.8387 | 累计平均损失: 0.6526\n",
      "Epoch: 18/20 | Batch: 700/782 | 单Batch损失: 0.6168 | 累计平均损失: 0.6502\n",
      "Epoch 18/20 完成 | 训练准确率: 77.20% | 测试准确率: 80.18%\n",
      "Epoch: 19/20 | Batch: 100/782 | 单Batch损失: 0.5368 | 累计平均损失: 0.6308\n",
      "Epoch: 19/20 | Batch: 200/782 | 单Batch损失: 0.4568 | 累计平均损失: 0.6476\n",
      "Epoch: 19/20 | Batch: 300/782 | 单Batch损失: 0.4435 | 累计平均损失: 0.6396\n",
      "Epoch: 19/20 | Batch: 400/782 | 单Batch损失: 0.5895 | 累计平均损失: 0.6407\n",
      "Epoch: 19/20 | Batch: 500/782 | 单Batch损失: 0.9253 | 累计平均损失: 0.6424\n",
      "Epoch: 19/20 | Batch: 600/782 | 单Batch损失: 0.5735 | 累计平均损失: 0.6417\n",
      "Epoch: 19/20 | Batch: 700/782 | 单Batch损失: 0.6145 | 累计平均损失: 0.6418\n",
      "Epoch    19: reducing learning rate of group 0 to 5.0000e-04.\n",
      "Epoch 19/20 完成 | 训练准确率: 77.65% | 测试准确率: 79.72%\n",
      "Epoch: 20/20 | Batch: 100/782 | 单Batch损失: 0.5315 | 累计平均损失: 0.5793\n",
      "Epoch: 20/20 | Batch: 200/782 | 单Batch损失: 0.5769 | 累计平均损失: 0.5837\n",
      "Epoch: 20/20 | Batch: 300/782 | 单Batch损失: 0.4930 | 累计平均损失: 0.5806\n",
      "Epoch: 20/20 | Batch: 400/782 | 单Batch损失: 0.5806 | 累计平均损失: 0.5821\n",
      "Epoch: 20/20 | Batch: 500/782 | 单Batch损失: 0.8352 | 累计平均损失: 0.5847\n",
      "Epoch: 20/20 | Batch: 600/782 | 单Batch损失: 0.5066 | 累计平均损失: 0.5831\n",
      "Epoch: 20/20 | Batch: 700/782 | 单Batch损失: 0.5890 | 累计平均损失: 0.5816\n",
      "Epoch 20/20 完成 | 训练准确率: 79.72% | 测试准确率: 81.85%\n"
     ]
    },
    {
     "data": {
      "image/png": "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oUIFJScnF5iAuVP2RF8pKSm2sb3ZmjVrpjNnztitnT1p0iRVrVpV//rXv4ose9++ffrhhx+0YcMG3XXXXZJkG0OeXxwlaQG/6aabZLFYtGHDBtvybadOndLBgwftJoUraCI0Z+rcubOio6NVs2ZN/fLLL/Lz88vzmWYbPXq0atSooTNnzthmQZdU6ER51atX1y233KKGDRuqZcuWatasmRo3bqygoCCNGDFC/v7++s9//mM7v3v37vrnP/+p559/XpIKTIKDgoLUunVr+fj46Ntvv1VAQIAt6a5Xr55at25dYEwXL15Ut27ddN1112nEiBFq37692rRpox49eigqKspufD8AwPuQdAMA3OrYsWM6e/ZsgbNtZ7v22mt13XXXad++fWUUmeOaNm2qkJAQvf3222rXrp327t2rXr16qXr16powYYLat2+vl156SR07dtT3339vm6zMEVdeeaUsFouWLl0qf39/rV+/3tb9OjMzU35+l/5X3rp1ay1YsEDNmjXT+fPnFRgYqPvvv7/IezRo0EADBw7UM888o6ysLNWqVUsTJkxQzZo1NWjQoJJ9KAWwWq06efKkbTtnC7BkrrfdokULSebkcrlbhCVzUrE//vhDn3/+ub755hvddddddhO65ezybuRaRqx9+/Zq3769JPMHjVWrVql69eoKDAxUVFSUVqxYYXd+//791bdvX91xxx269dZbC3xfLVu21LZt25SQkKBnnnlGjzzyiNasWaOPPvpIU6ZM0bx58/K9btu2bRo4cKB8fX21Zs0a+fj46Oqrr9YPP/yg++67T40aNdKoUaM0atQokm8A8FL0WQIAuFWXLl2UkpKS7yM5OVlfffWVgoKCdPHiRX333XcOlWm1Wp22LJgjQkNDtWjRIi1cuFB33323pk+fbrv/3XffrSVLlmj58uXq0KGDPv74Y3344Yfq2bOnQ2XXqVNHs2bN0po1a3T//fdry5YttrWgc38eb731lsLCwtSxY0cNGjRIZ8+edfg9zJo1S8OHD9dLL72kXr16qXbt2tq8eXO+SW9JZSfcderUsT22bdsmq9Wa7/l79+7Nd0jBjh07lJmZqQULFmjKlCmqUaOGbV1rSWrXrp06duwoyUzQs2cal8y1vh999FHVqVNHbdq00U8//aRDhw5p8ODB+uijj3TvvfdKMnst+Pj46MEHH1TPnj01YMAA2w8EZ86c0Z49eyTJbh3v9evXq3Xr1vL19dWUKVNUq1YtpaamqmnTphozZozdTPA7duzQ0KFDddttt6lOnTravHmz3Xu9+uqr9fPPP+uxxx7T66+/roiICHXq1EmZmZnF/twBAG7m1mncAAAoQEpKihEREWFIMkaOHFmsa6+77jrjhRdecFFkKKlZs2blmUm8bdu2xosvvmjbPnjwoDFgwACjS5cuRlBQkPHSSy/lKeell14yXnjhBSM+Pt544IEHjLVr1xZ4z9WrVxtVqlSxbd97773G3XffbSxdutRIS0sz0tLSjAYNGhjvv/++YRiGERkZadx5552GJGP79u2GYRjGH3/8Ydx8883G4cOHDcMwjDvuuMOQZLRu3drIysoyoqKijPr16xs+Pj5G//79jdjYWLsYPvvsM6NKlSrGgw8+aBiGYezZs8cICgoyatWqZXz44YdGVlZWoZ/b//3f/xkDBw60zfIOAPAuFsPI1e8KAAAPsX37dlWpUkWNGjUq1nXXXHONOnfurGnTprkoMpTEr7/+qv3796t79+62fXv37lVISIht5nHDMNSlSxfVqlVLnTt3VpcuXfItKysry6FJxqxWq1JTU23rZKelpeWZvC02NtbWyvzTTz/pk08+0d1336377ruvwPcREBBgi/nChQuaMmWKBg0aVOBs+qdPn5Yk23r0O3bsUPPmzYtcWxwA4P1IugEAAAAAcBG3jun+66+/9MMPPygmJsadYQAAAAAA4BJuS7qXLFmiRo0a6V//+pfq1q2rJUuWFHnN5s2bde211yo8PFzTp08vgygBAAAAACg5tyTdFy9e1L///W9t3bpVP//8s2bNmmVb/7Ig58+fV9euXfXwww/rxx9/1MKFC7Vx48YyihgAAAAAgOJzS9KdkJCgGTNmqFmzZpKkf/zjH4qNjS30moULFyoiIkLjx49X48aNNWHCBEVFRZVFuAAAAAAAlIjbJ1LLyMjQ4MGD5ePjo48++qjA8wYOHKjg4GDNnDlTkrlG5t133639+/c7dJ+srCydPn1aISEhslgszggdAAAAAHCZMgxDCQkJqlmzZqEraviVYUx57NmzR+3atVNAQIAOHjxY6Lnx8fFq2rSpbTs0NFSnTp0q8Py0tDSlpaXZtk+dOmV3PQAAAAAApXXixAnVrl27wONuTbqbN2+uDRs26JlnntHA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LpWKFi0qTZs2TVq8eLHUpk0bqUKFCvwODEJQRO51twxYdv6hLCut+4z8/HyWVRBQlpXefQbLKvCUZaV1n3H06NGgKytWuoPEihUrpKJFi0olS5aUEhMTpb179wY6S4XKihUrJAAOP0ePHtUtG3fXkff06dPHZioPllVw+e2336TmzZtLRYsWlSpXriytWLFCkiSWVbDJz8+Xxo4dK1WoUEGKiIiQ6tevL23btk2SJJZVsIHddDm+KB+WnXfAbno3rfsMeT3LKnDsP1dKyvsMSWJZBZp9WWndZ0hScJWV6d/MUxA4deoUtm3bhubNmyM5OTnQ2SEFvbJxdx35BssqdLCsQgfLKrj5onxYdv7HsgodLKvQESxlxUo3ERERERERkY8wkBoRERERERGRj7DSTUREREREROQjrHQTERERERER+Qgr3URERBRyDh48iHXr1gU6G0RERE6x0k1ERFTIHTx4EC1btsStW7cCnRXD1q5di6efftrpdiNHjkSJEiVQtWpVh5+iRYviq6++stn+1q1baNmyJQ4ePOirrBMRUSHDSjcRERUaP/30E+Lj4wOdDV2ffPIJ7r77br8d78yZM+jatSsmTJiAyMhIAMDdd98Nk8kEk8mExMRE9OnTB9euXfNZHu6++2588sknmuvHjBmDunXr2vxMnToVR44cQZ06dVC3bl3UqVMH1atXx8SJE232jYyMRL9+/XDo0CGHnyZNmljOWbn9hAkT0KVLF5w5c8YXp0tERIUMK91ERFToVapUCT/99JPfjmcymXDs2DHVdY899hi++eYbv+WlT58+mDRpElq3bm2z/K233kJmZiY2b96M/fv34+233zaU3k8//YRKlSp5NY8ZGRlo27Yt9uzZgz179mDv3r04cuQIJEnCn3/+ib1791rWDR061GbfsLAw5OXl4ebNmw4/+fn5UJs5tXXr1pg0aRJ69+7t1fMgIqLCKTzQGSAiIiKryMhIh9ZXX9m0aROioqJw//33O6wrWrQo4uPjER8fj27dumHjxo1+yZOaiIgImEwmbNmyBV27dkWxYsUQHm69hbl48SKuX7+OtWvX4q677rLZNzY2FgsWLMCqVatU09a61t26dcOHH36IjRs3omXLlt47GSIiKnTY0k1ERIVWx44dYTKZcPz4cbRp0wYmkwmTJk2yrP/uu+9w2223IT4+Hv3790d2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",
      "text/plain": [
       "<Figure size 1000x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "训练完成！最终测试准确率: 81.85%\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "from torch.utils.tensorboard import SummaryWriter  \n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import os\n",
    "import torchvision  # 记得导入 torchvision，之前代码里用到了其功能但没导入\n",
    "\n",
    "# 设置中文字体支持\n",
    "plt.rcParams[\"font.family\"] = [\"SimHei\"]\n",
    "plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题\n",
    "\n",
    "# 检查GPU是否可用\n",
    "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "print(f\"使用设备: {device}\")\n",
    "\n",
    "# 1. 数据预处理 \n",
    "train_transform = transforms.Compose([\n",
    "    transforms.RandomCrop(32, padding=4),\n",
    "    transforms.RandomHorizontalFlip(),\n",
    "    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),\n",
    "    transforms.RandomRotation(15),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))\n",
    "])\n",
    "\n",
    "test_transform = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))\n",
    "])\n",
    "\n",
    "# 2. 加载CIFAR-10数据集\n",
    "train_dataset = datasets.CIFAR10(\n",
    "    root='./data',\n",
    "    train=True,\n",
    "    download=True,\n",
    "    transform=train_transform\n",
    ")\n",
    "\n",
    "test_dataset = datasets.CIFAR10(\n",
    "    root='./data',\n",
    "    train=False,\n",
    "    transform=test_transform\n",
    ")\n",
    "\n",
    "# 3. 创建数据加载器\n",
    "batch_size = 64\n",
    "train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)\n",
    "\n",
    "\n",
    "\n",
    "# 4. 定义CNN模型的定义（替代原MLP）\n",
    "class CNN(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(CNN, self).__init__()  # 继承父类初始化\n",
    "        \n",
    "        # ---------------------- 第一个卷积块 ----------------------\n",
    "        # 卷积层1：输入3通道（RGB），输出32个特征图，卷积核3x3，边缘填充1像素\n",
    "        self.conv1 = nn.Conv2d(\n",
    "            in_channels=3,       # 输入通道数（图像的RGB通道）\n",
    "            out_channels=32,     # 输出通道数（生成32个新特征图）\n",
    "            kernel_size=3,       # 卷积核尺寸（3x3像素）\n",
    "            padding=1            # 边缘填充1像素，保持输出尺寸与输入相同\n",
    "        )\n",
    "        # 批量归一化层：对32个输出通道进行归一化，加速训练\n",
    "        self.bn1 = nn.BatchNorm2d(num_features=32)\n",
    "        # ReLU激活函数：引入非线性，公式：max(0, x)\n",
    "        self.relu1 = nn.ReLU()\n",
    "        # 最大池化层：窗口2x2，步长2，特征图尺寸减半（32x32→16x16）\n",
    "        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)  # stride默认等于kernel_size\n",
    "        \n",
    "        # ---------------------- 第二个卷积块 ----------------------\n",
    "        # 卷积层2：输入32通道（来自conv1的输出），输出64通道\n",
    "        self.conv2 = nn.Conv2d(\n",
    "            in_channels=32,      # 输入通道数（前一层的输出通道数）\n",
    "            out_channels=64,     # 输出通道数（特征图数量翻倍）\n",
    "            kernel_size=3,       # 卷积核尺寸不变\n",
    "            padding=1            # 保持尺寸：16x16→16x16（卷积后）→8x8（池化后）\n",
    "        )\n",
    "        self.bn2 = nn.BatchNorm2d(num_features=64)\n",
    "        self.relu2 = nn.ReLU()\n",
    "        self.pool2 = nn.MaxPool2d(kernel_size=2)  # 尺寸减半：16x16→8x8\n",
    "        \n",
    "        # ---------------------- 第三个卷积块 ----------------------\n",
    "        # 卷积层3：输入64通道，输出128通道\n",
    "        self.conv3 = nn.Conv2d(\n",
    "            in_channels=64,      # 输入通道数（前一层的输出通道数）\n",
    "            out_channels=128,    # 输出通道数（特征图数量再次翻倍）\n",
    "            kernel_size=3,\n",
    "            padding=1            # 保持尺寸：8x8→8x8（卷积后）→4x4（池化后）\n",
    "        )\n",
    "        self.bn3 = nn.BatchNorm2d(num_features=128)\n",
    "        self.relu3 = nn.ReLU()  # 复用激活函数对象（节省内存）\n",
    "        self.pool3 = nn.MaxPool2d(kernel_size=2)  # 尺寸减半：8x8→4x4\n",
    "        \n",
    "        # ---------------------- 全连接层（分类器） ----------------------\n",
    "        # 计算展平后的特征维度：128通道 × 4x4尺寸 = 128×16=2048维\n",
    "        self.fc1 = nn.Linear(\n",
    "            in_features=128 * 4 * 4,  # 输入维度（卷积层输出的特征数）\n",
    "            out_features=512          # 输出维度（隐藏层神经元数）\n",
    "        )\n",
    "        # Dropout层：训练时随机丢弃50%神经元，防止过拟合\n",
    "        self.dropout = nn.Dropout(p=0.5)\n",
    "        # 输出层：将512维特征映射到10个类别（CIFAR-10的类别数）\n",
    "        self.fc2 = nn.Linear(in_features=512, out_features=10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        # 输入尺寸：[batch_size, 3, 32, 32]（batch_size=批量大小，3=通道数，32x32=图像尺寸）\n",
    "        \n",
    "        # ---------- 卷积块1处理 ----------\n",
    "        x = self.conv1(x)       # 卷积后尺寸：[batch_size, 32, 32, 32]（padding=1保持尺寸）\n",
    "        x = self.bn1(x)         # 批量归一化，不改变尺寸\n",
    "        x = self.relu1(x)       # 激活函数，不改变尺寸\n",
    "        x = self.pool1(x)       # 池化后尺寸：[batch_size, 32, 16, 16]（32→16是因为池化窗口2x2）\n",
    "        \n",
    "        # ---------- 卷积块2处理 ----------\n",
    "        x = self.conv2(x)       # 卷积后尺寸：[batch_size, 64, 16, 16]（padding=1保持尺寸）\n",
    "        x = self.bn2(x)\n",
    "        x = self.relu2(x)\n",
    "        x = self.pool2(x)       # 池化后尺寸：[batch_size, 64, 8, 8]\n",
    "        \n",
    "        # ---------- 卷积块3处理 ----------\n",
    "        x = self.conv3(x)       # 卷积后尺寸：[batch_size, 128, 8, 8]（padding=1保持尺寸）\n",
    "        x = self.bn3(x)\n",
    "        x = self.relu3(x)\n",
    "        x = self.pool3(x)       # 池化后尺寸：[batch_size, 128, 4, 4]\n",
    "        \n",
    "        # ---------- 展平与全连接层 ----------\n",
    "        # 将多维特征图展平为一维向量：[batch_size, 128*4*4] = [batch_size, 2048]\n",
    "        x = x.view(-1, 128 * 4 * 4)  # -1自动计算批量维度，保持批量大小不变\n",
    "        \n",
    "        x = self.fc1(x)           # 全连接层：2048→512，尺寸变为[batch_size, 512]\n",
    "        x = self.relu3(x)         # 激活函数（复用relu3，与卷积块3共用）\n",
    "        x = self.dropout(x)       # Dropout随机丢弃神经元，不改变尺寸\n",
    "        x = self.fc2(x)           # 全连接层：512→10，尺寸变为[batch_size, 10]（未激活，直接输出logits）\n",
    "        \n",
    "        return x  # 输出未经过Softmax的logits，适用于交叉熵损失函数\n",
    "\n",
    "\n",
    "\n",
    "# 初始化模型\n",
    "model = CNN()\n",
    "model = model.to(device)  # 将模型移至GPU（如果可用）\n",
    "\n",
    "\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.001)\n",
    "scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n",
    "    optimizer,        # 指定要控制的优化器（这里是Adam）\n",
    "    mode='min',       # 监测的指标是\"最小化\"（如损失函数）\n",
    "    patience=3,       # 如果连续3个epoch指标没有改善，才降低LR\n",
    "    factor=0.5,       # 降低LR的比例（新LR = 旧LR × 0.5）\n",
    "    verbose=True      # 打印学习率调整信息\n",
    ")\n",
    "\n",
    "# ======================== TensorBoard 核心配置 ========================\n",
    "# 创建 TensorBoard 日志目录（自动避免重复）\n",
    "log_dir = \"runs/cifar10_cnn_exp\"\n",
    "if os.path.exists(log_dir):\n",
    "    version = 1\n",
    "    while os.path.exists(f\"{log_dir}_v{version}\"):\n",
    "        version += 1\n",
    "    log_dir = f\"{log_dir}_v{version}\"\n",
    "writer = SummaryWriter(log_dir)  # 初始化 SummaryWriter\n",
    "\n",
    "# 5. 训练模型（整合 TensorBoard 记录）\n",
    "def train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, writer):\n",
    "    model.train()\n",
    "    all_iter_losses = []  \n",
    "    iter_indices = []     \n",
    "    global_step = 0       # 全局步骤，用于 TensorBoard 标量记录\n",
    "\n",
    "    # （可选）记录模型结构：用一个真实样本走一遍前向传播，让 TensorBoard 解析计算图\n",
    "    dataiter = iter(train_loader)\n",
    "    images, labels = next(dataiter)\n",
    "    images = images.to(device)\n",
    "    writer.add_graph(model, images)  # 写入模型结构到 TensorBoard\n",
    "\n",
    "    # （可选）记录原始训练图像：可视化数据增强前/后效果\n",
    "    img_grid = torchvision.utils.make_grid(images[:8].cpu())  # 取前8张\n",
    "    writer.add_image('原始训练图像（增强前）', img_grid, global_step=0)\n",
    "\n",
    "    for epoch in range(epochs):\n",
    "        running_loss = 0.0\n",
    "        correct = 0\n",
    "        total = 0\n",
    "        \n",
    "        for batch_idx, (data, target) in enumerate(train_loader):\n",
    "            data, target = data.to(device), target.to(device)\n",
    "            \n",
    "            optimizer.zero_grad()\n",
    "            output = model(data)\n",
    "            loss = criterion(output, target)\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "\n",
    "            # 记录迭代级损失\n",
    "            iter_loss = loss.item()\n",
    "            all_iter_losses.append(iter_loss)\n",
    "            iter_indices.append(global_step + 1)  # 用 global_step 对齐\n",
    "\n",
    "            # 统计准确率\n",
    "            running_loss += iter_loss\n",
    "            _, predicted = output.max(1)\n",
    "            total += target.size(0)\n",
    "            correct += predicted.eq(target).sum().item()\n",
    "\n",
    "            # ======================== TensorBoard 标量记录 ========================\n",
    "            # 记录每个 batch 的损失、准确率\n",
    "            batch_acc = 100. * correct / total\n",
    "            writer.add_scalar('Train/Batch Loss', iter_loss, global_step)\n",
    "            writer.add_scalar('Train/Batch Accuracy', batch_acc, global_step)\n",
    "\n",
    "            # 记录学习率（可选）\n",
    "            writer.add_scalar('Train/Learning Rate', optimizer.param_groups[0]['lr'], global_step)\n",
    "\n",
    "            # 每 200 个 batch 记录一次参数直方图（可选，耗时稍高）\n",
    "            if (batch_idx + 1) % 200 == 0:\n",
    "                for name, param in model.named_parameters():\n",
    "                    writer.add_histogram(f'Weights/{name}', param, global_step)\n",
    "                    if param.grad is not None:\n",
    "                        writer.add_histogram(f'Gradients/{name}', param.grad, global_step)\n",
    "\n",
    "            # 每 100 个 batch 打印控制台日志（同原代码）\n",
    "            if (batch_idx + 1) % 100 == 0:\n",
    "                print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} '\n",
    "                      f'| 单Batch损失: {iter_loss:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}')\n",
    "\n",
    "            global_step += 1  # 全局步骤递增\n",
    "\n",
    "        # 计算 epoch 级训练指标\n",
    "        epoch_train_loss = running_loss / len(train_loader)\n",
    "        epoch_train_acc = 100. * correct / total\n",
    "\n",
    "        # ======================== TensorBoard  epoch 标量记录 ========================\n",
    "        writer.add_scalar('Train/Epoch Loss', epoch_train_loss, epoch)\n",
    "        writer.add_scalar('Train/Epoch Accuracy', epoch_train_acc, epoch)\n",
    "\n",
    "        # 测试阶段\n",
    "        model.eval()\n",
    "        test_loss = 0\n",
    "        correct_test = 0\n",
    "        total_test = 0\n",
    "        wrong_images = []  # 存储错误预测样本（用于可视化）\n",
    "        wrong_labels = []\n",
    "        wrong_preds = []\n",
    "\n",
    "        with torch.no_grad():\n",
    "            for data, target in test_loader:\n",
    "                data, target = data.to(device), target.to(device)\n",
    "                output = model(data)\n",
    "                test_loss += criterion(output, target).item()\n",
    "                _, predicted = output.max(1)\n",
    "                total_test += target.size(0)\n",
    "                correct_test += predicted.eq(target).sum().item()\n",
    "\n",
    "                # 收集错误预测样本（用于可视化）\n",
    "                wrong_mask = (predicted != target)\n",
    "                if wrong_mask.sum() > 0:\n",
    "                    wrong_batch_images = data[wrong_mask][:8].cpu()  # 最多存8张\n",
    "                    wrong_batch_labels = target[wrong_mask][:8].cpu()\n",
    "                    wrong_batch_preds = predicted[wrong_mask][:8].cpu()\n",
    "                    wrong_images.extend(wrong_batch_images)\n",
    "                    wrong_labels.extend(wrong_batch_labels)\n",
    "                    wrong_preds.extend(wrong_batch_preds)\n",
    "\n",
    "        # 计算 epoch 级测试指标\n",
    "        epoch_test_loss = test_loss / len(test_loader)\n",
    "        epoch_test_acc = 100. * correct_test / total_test\n",
    "\n",
    "        # ======================== TensorBoard 测试集记录 ========================\n",
    "        writer.add_scalar('Test/Epoch Loss', epoch_test_loss, epoch)\n",
    "        writer.add_scalar('Test/Epoch Accuracy', epoch_test_acc, epoch)\n",
    "\n",
    "        # （可选）可视化错误预测样本\n",
    "        if wrong_images:\n",
    "            wrong_img_grid = torchvision.utils.make_grid(wrong_images)\n",
    "            writer.add_image('错误预测样本', wrong_img_grid, epoch)\n",
    "            # 写入错误标签文本（可选）\n",
    "            wrong_text = [f\"真实: {classes[wl]}, 预测: {classes[wp]}\" \n",
    "                         for wl, wp in zip(wrong_labels, wrong_preds)]\n",
    "            writer.add_text('错误预测标签', '\\n'.join(wrong_text), epoch)\n",
    "\n",
    "        # 更新学习率调度器\n",
    "        scheduler.step(epoch_test_loss)\n",
    "\n",
    "        print(f'Epoch {epoch+1}/{epochs} 完成 | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%')\n",
    "\n",
    "    # 关闭 TensorBoard 写入器\n",
    "    writer.close()\n",
    "\n",
    "    # 绘制迭代级损失曲线（同原代码）\n",
    "    plot_iter_losses(all_iter_losses, iter_indices)\n",
    "    return epoch_test_acc\n",
    "\n",
    "# 6. 绘制迭代级损失曲线（同原代码，略）\n",
    "def plot_iter_losses(losses, indices):\n",
    "    plt.figure(figsize=(10, 4))\n",
    "    plt.plot(indices, losses, 'b-', alpha=0.7, label='Iteration Loss')\n",
    "    plt.xlabel('Iteration（Batch序号）')\n",
    "    plt.ylabel('损失值')\n",
    "    plt.title('每个 Iteration 的训练损失')\n",
    "    plt.legend()\n",
    "    plt.grid(True)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "# （可选）CIFAR-10 类别名\n",
    "classes = ('plane', 'car', 'bird', 'cat',\n",
    "           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')\n",
    "\n",
    "# 7. 执行训练（传入 TensorBoard writer）\n",
    "epochs = 20\n",
    "print(\"开始使用CNN训练模型...\")\n",
    "print(f\"TensorBoard 日志目录: {log_dir}\")\n",
    "print(\"训练后执行: tensorboard --logdir=runs 查看可视化\")\n",
    "\n",
    "final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, writer)\n",
    "print(f\"训练完成！最终测试准确率: {final_accuracy:.2f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12188b6e",
   "metadata": {},
   "source": [
    "由于已近搭载了tensorboard，上述代码中一些之前可视化的冗余部分可以删除了"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "b95a0be4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TensorBoard 日志目录: runs/cifar10_cnn_exp_v2\n",
      "开始使用CNN训练模型...\n",
      "训练后执行: tensorboard --logdir=runs 查看可视化\n",
      "Epoch    21: reducing learning rate of group 0 to 5.0000e-04.\n",
      "Epoch 1/20 完成 | 测试准确率: 75.81%\n",
      "Epoch 2/20 完成 | 测试准确率: 80.88%\n",
      "Epoch 3/20 完成 | 测试准确率: 80.36%\n",
      "Epoch 4/20 完成 | 测试准确率: 82.32%\n",
      "Epoch 5/20 完成 | 测试准确率: 80.98%\n",
      "Epoch 6/20 完成 | 测试准确率: 81.43%\n",
      "Epoch 7/20 完成 | 测试准确率: 81.86%\n",
      "Epoch    28: reducing learning rate of group 0 to 2.5000e-04.\n",
      "Epoch 8/20 完成 | 测试准确率: 81.89%\n",
      "Epoch 9/20 完成 | 测试准确率: 82.69%\n",
      "Epoch 10/20 完成 | 测试准确率: 83.66%\n",
      "Epoch 11/20 完成 | 测试准确率: 83.29%\n",
      "Epoch 12/20 完成 | 测试准确率: 82.99%\n",
      "Epoch 13/20 完成 | 测试准确率: 83.11%\n",
      "Epoch    34: reducing learning rate of group 0 to 1.2500e-04.\n",
      "Epoch 14/20 完成 | 测试准确率: 83.58%\n",
      "Epoch 15/20 完成 | 测试准确率: 83.79%\n",
      "Epoch 16/20 完成 | 测试准确率: 83.88%\n",
      "Epoch 17/20 完成 | 测试准确率: 83.89%\n",
      "Epoch 18/20 完成 | 测试准确率: 84.08%\n",
      "Epoch 19/20 完成 | 测试准确率: 84.25%\n",
      "Epoch 20/20 完成 | 测试准确率: 84.21%\n",
      "训练完成！最终测试准确率: 84.21%\n"
     ]
    }
   ],
   "source": [
    "# 省略预处理、模型定义代码\n",
    "\n",
    "# ======================== TensorBoard 核心配置 ========================\n",
    "# 在使用tensorboard前需要先指定日志保存路径\n",
    "log_dir = \"runs/cifar10_cnn_exp\" # 指定日志保存路径\n",
    "if os.path.exists(log_dir): #检查刚才定义的路径是否存在\n",
    "    version = 1 \n",
    "    while os.path.exists(f\"{log_dir}_v{version}\"): # 如果路径存在且版本号一致\n",
    "        version += 1 # 版本号加1\n",
    "    log_dir = f\"{log_dir}_v{version}\" # 如果路径存在，则创建一个新版本\n",
    "writer = SummaryWriter(log_dir) # 初始化SummaryWriter\n",
    "print(f\"TensorBoard 日志目录: {log_dir}\") # 所以第一次是cifar10_cnn_exp、第二次是cifar10_cnn_exp_v1\n",
    "\n",
    "# 5. 训练模型（整合 TensorBoard 记录）\n",
    "def train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, writer):\n",
    "    model.train()\n",
    "    global_step = 0  # 全局步骤，用于 TensorBoard 标量记录\n",
    "\n",
    "    # 记录模型结构和训练图像\n",
    "    dataiter = iter(train_loader)\n",
    "    images, labels = next(dataiter)\n",
    "    images = images.to(device)\n",
    "    writer.add_graph(model, images)\n",
    "    \n",
    "    img_grid = torchvision.utils.make_grid(images[:8].cpu())\n",
    "    writer.add_image('原始训练图像（增强前）', img_grid, global_step=0)\n",
    "\n",
    "    for epoch in range(epochs):\n",
    "        running_loss = 0.0\n",
    "        correct = 0\n",
    "        total = 0\n",
    "        \n",
    "        for batch_idx, (data, target) in enumerate(train_loader):\n",
    "            data, target = data.to(device), target.to(device)\n",
    "            \n",
    "            optimizer.zero_grad()\n",
    "            output = model(data)\n",
    "            loss = criterion(output, target)\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "\n",
    "            # 统计准确率\n",
    "            running_loss += loss.item()\n",
    "            _, predicted = output.max(1)\n",
    "            total += target.size(0)\n",
    "            correct += predicted.eq(target).sum().item()\n",
    "\n",
    "            # 记录每个 batch 的损失、准确率和学习率\n",
    "            batch_acc = 100. * correct / total\n",
    "            writer.add_scalar('Train/Batch Loss', loss.item(), global_step)\n",
    "            writer.add_scalar('Train/Batch Accuracy', batch_acc, global_step)\n",
    "            writer.add_scalar('Train/Learning Rate', optimizer.param_groups[0]['lr'], global_step)\n",
    "\n",
    "            # 每 200 个 batch 记录一次参数直方图\n",
    "            if (batch_idx + 1) % 200 == 0:\n",
    "                for name, param in model.named_parameters():\n",
    "                    writer.add_histogram(f'Weights/{name}', param, global_step)\n",
    "                    if param.grad is not None:\n",
    "                        writer.add_histogram(f'Gradients/{name}', param.grad, global_step)\n",
    "\n",
    "            global_step += 1\n",
    "\n",
    "        # 计算 epoch 级训练指标\n",
    "        epoch_train_loss = running_loss / len(train_loader)\n",
    "        epoch_train_acc = 100. * correct / total\n",
    "        writer.add_scalar('Train/Epoch Loss', epoch_train_loss, epoch)\n",
    "        writer.add_scalar('Train/Epoch Accuracy', epoch_train_acc, epoch)\n",
    "\n",
    "        # 测试阶段\n",
    "        model.eval()\n",
    "        test_loss = 0\n",
    "        correct_test = 0\n",
    "        total_test = 0\n",
    "        wrong_images = []\n",
    "        wrong_labels = []\n",
    "        wrong_preds = []\n",
    "\n",
    "        with torch.no_grad():\n",
    "            for data, target in test_loader:\n",
    "                data, target = data.to(device), target.to(device)\n",
    "                output = model(data)\n",
    "                test_loss += criterion(output, target).item()\n",
    "                _, predicted = output.max(1)\n",
    "                total_test += target.size(0)\n",
    "                correct_test += predicted.eq(target).sum().item()\n",
    "\n",
    "                # 收集错误预测样本\n",
    "                wrong_mask = (predicted != target)\n",
    "                if wrong_mask.sum() > 0:\n",
    "                    wrong_batch_images = data[wrong_mask][:8].cpu()\n",
    "                    wrong_batch_labels = target[wrong_mask][:8].cpu()\n",
    "                    wrong_batch_preds = predicted[wrong_mask][:8].cpu()\n",
    "                    wrong_images.extend(wrong_batch_images)\n",
    "                    wrong_labels.extend(wrong_batch_labels)\n",
    "                    wrong_preds.extend(wrong_batch_preds)\n",
    "\n",
    "        # 计算 epoch 级测试指标\n",
    "        epoch_test_loss = test_loss / len(test_loader)\n",
    "        epoch_test_acc = 100. * correct_test / total_test\n",
    "        writer.add_scalar('Test/Epoch Loss', epoch_test_loss, epoch)\n",
    "        writer.add_scalar('Test/Epoch Accuracy', epoch_test_acc, epoch)\n",
    "\n",
    "        # 可视化错误预测样本\n",
    "        if wrong_images:\n",
    "            wrong_img_grid = torchvision.utils.make_grid(wrong_images)\n",
    "            writer.add_image('错误预测样本', wrong_img_grid, epoch)\n",
    "            wrong_text = [f\"真实: {classes[wl]}, 预测: {classes[wp]}\" \n",
    "                         for wl, wp in zip(wrong_labels, wrong_preds)]\n",
    "            writer.add_text('错误预测标签', '\\n'.join(wrong_text), epoch)\n",
    "\n",
    "        # 更新学习率调度器\n",
    "        scheduler.step(epoch_test_loss)\n",
    "        print(f'Epoch {epoch+1}/{epochs} 完成 | 测试准确率: {epoch_test_acc:.2f}%')\n",
    "\n",
    "    writer.close()\n",
    "    return epoch_test_acc\n",
    "\n",
    "# （可选）CIFAR-10 类别名\n",
    "classes = ('plane', 'car', 'bird', 'cat',\n",
    "           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')\n",
    "\n",
    "# 执行训练\n",
    "epochs = 20\n",
    "print(\"开始使用CNN训练模型...\")\n",
    "print(\"训练后执行: tensorboard --logdir=runs 查看可视化\")\n",
    "\n",
    "final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, scheduler, device, epochs, writer)\n",
    "print(f\"训练完成！最终测试准确率: {final_accuracy:.2f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93de3818",
   "metadata": {},
   "source": [
    "上述这段代码，由于我单独拎出来了，没有重新初始化cnn，如果二次运行就会创建一个新的目录，并且接着之前的运行"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "157b5e1f",
   "metadata": {},
   "source": [
    "tensorboard的代码还有有一定的记忆量，实际上深度学习的经典代码都是类似于八股文，看多了就习惯了，难度远远小于考研数学等需要思考的内容\n",
    "\n",
    "实际上对目前的ai而言，你只需要先完成最简单的demo，然后让他给你加上tensorboard需要打印的部分即可。---核心是弄懂tensorboard可以打印什么信息，以及如何看可视化后的结果，把ai当成记忆大师用到的时候通过它来调取对应的代码即可。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "yolov5_new",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
